﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;


namespace Boilen.Primitives {

    /// <summary>
    /// Retrieves XML documentation for members from existing .NET libraries.
    /// </summary>
    /// <seealso href="http://stackoverflow.com/questions/230925/retrieve-xml-doc-comments-programmatically"/>
    public static class XmlDocumentation {

        public static string CombinePath( params string[] pathSegments ) {
            Ensure.NotNullOrEmpty( pathSegments );

            var path = pathSegments.Aggregate( System.IO.Path.Combine );
            return path;
        }


        public static string GetMemberId( MemberInfo member ) {
            Ensure.NotNull( member );

            char memberKindPrefix = GetMemberPrefix( member );
            string memberName = GetMemberFullName( member );
            return memberKindPrefix + ":" + memberName;
        }

        public static string GetElementName( XElement element ) {
            string elementAttributes = Util.Join( element.Attributes( ), "", ( i, a ) => " " + a.ToString( ) );
            string elementName = element.Name.LocalName + elementAttributes;
            return RemoveRawReferences( elementName );
        }

        public static string GetElementValue( XElement element ) {
            Ensure.NotNull( element );

            string[] nodeValues = Util.Enumerate( element.FirstNode, n => n.NextNode )
                .Select( n => RemoveRawReferences( n.ToString( ).Trim( ) ) )
                .Where( v => v.Length > 0 )
                .ToArray( );

            string value = Util.Join(
                nodeValues,
                ( i, last ) => char.IsPunctuation( nodeValues[i], 0 ) ? "" : " ",
                ( i, v ) => v
            );

            return value;
        }

        public static IEnumerable<XElement> FilterElements( IEnumerable<XElement> elements, params string[] excludedNames ) {
            Ensure.NotNull( elements, excludedNames );

            return elements.Where( e => !excludedNames.Contains( e.Name.LocalName ) );
        }

        public static IEnumerable<XElement> FilterElements( XElement element, params string[] excludedNames ) {
            Ensure.NotNull( element );
            return FilterElements( element.Elements( ), excludedNames );
        }


        public static bool HasXmlDocumentation( Assembly assembly ) {
            return GetXmlDocFile( assembly ) != null;
        }

        public static XElement GetDocMember( MemberInfo member ) {
            Ensure.NotNull( member );

            Assembly assembly = (member.DeclaringType ?? (Type)member).Assembly;
            Ensure.ArgSatisfies( HasXmlDocumentation( assembly ), "member", "Member is undocumented." );

            XDocument docFile = GetXmlDocFile( assembly );
            string memberId = GetMemberId( member );

            var docMembers =
                from element in docFile.Root.Element( "members" ).Elements( "member" )
                let name = element.Attribute( "name" ).Value
                where name == memberId
                select element;

            var docMember = docMembers.First( );
            return docMember;
        }

        public static string GetSummary( MemberInfo member ) {
            var xmldoc = XmlDocumentation.GetDocMember( member );
            var summary = xmldoc.Element( "summary" );
            return summary.Value.Trim( );
        }


        #region Private

        private static readonly Dictionary<string, XDocument> DocumentationCache = new Dictionary<string, XDocument>( );

        private static readonly string[] SystemSearchDirectories;
        private static readonly string[] FallbackDirectoryNames;

        static XmlDocumentation( ) {
            // Reference a type from required assembly System.Xml.
            new System.Xml.NameTable( );

            var systemDir = new System.IO.DirectoryInfo( Environment.GetFolderPath( Environment.SpecialFolder.System ) );
            string frameworkDirPath = CombinePath( systemDir.Parent.FullName, "Microsoft.NET", "Framework" );
            var frameworkDirChildren = System.IO.Directory.GetDirectories( frameworkDirPath ).OrderByDescending( path => path );

            string referenceAssembliesDirPath = CombinePath( Environment.GetFolderPath( Environment.SpecialFolder.ProgramFiles ), "Reference Assemblies", "Microsoft", "Framework" );
            var referenceAssembliesDirChildren = System.IO.Directory.GetDirectories( referenceAssembliesDirPath ).OrderByDescending( path => path );

            SystemSearchDirectories = frameworkDirChildren
                .Concat( referenceAssembliesDirChildren )
                .ToArray( );


            var fallbackNames = new List<string>( );
            var culture = System.Globalization.CultureInfo.CurrentCulture;
            while( !fallbackNames.Contains( culture.Name ) ) {
                fallbackNames.Add( culture.Name );
                culture = culture.Parent;
            }
            FallbackDirectoryNames = fallbackNames.ToArray( );
        }


        private static char GetMemberPrefix( MemberInfo member ) {
            string memberTypeName = member.GetType( ).Name;
            char initial = memberTypeName
                .Replace( "Runtime", "" )
                .Replace( "Rt", "" )
                .Replace( "Md", "" )
                [0];

            // Correction for ConstructorInfo.
            initial = (initial == 'C' ? 'M' : initial);

            return initial;
        }

        private static string GetMemberFullName( MemberInfo member ) {
            Type type = member as Type;
            if( type != null && type.IsGenericParameter ) {
                string prefix;
                Type[] parameters;

                Type[] methodParameters = type.DeclaringMethod != null ? type.DeclaringMethod.GetGenericArguments( ) : Type.EmptyTypes;
                if( methodParameters.Contains( type ) ) {
                    parameters = methodParameters;
                    prefix = "``";
                }
                else {
                    parameters = type.DeclaringType.GetGenericArguments( );
                    prefix = "`";
                }

                return prefix + Array.IndexOf( parameters, type );
            }

            string memberScope = "";
            if( member.DeclaringType != null )
                memberScope = GetMemberFullName( member.DeclaringType );
            else if( type != null )
                memberScope = type.Namespace;

            string memberName = member.Name;
            string arguments = "";
            if( member is MethodBase ) {
                if( member is ConstructorInfo ) {
                    memberName = ((ConstructorInfo)member).IsStatic ? "#cctor" : "#ctor";
                }
                else {
                    int genericArgCount = ((MethodInfo)member).GetGenericArguments( ).Length;
                    if( genericArgCount > 0 )
                        memberName += "``" + genericArgCount;
                }

                arguments = Util.Join( ((MethodBase)member).GetParameters( ), ",", ( i, p ) => GetMemberFullName( p.ParameterType ) );
            }
            if( arguments.Length > 0 )
                arguments = "(" + arguments + ")";

            return memberScope + "." + memberName + arguments;
        }


        private static string RemoveRawReferences( string value ) {
            const string RawReferencePrefix = " cref=\"";

            for( int i = value.IndexOf( RawReferencePrefix ); i > 0; i = value.IndexOf( RawReferencePrefix, i + 1 ) ) {
                int colonIndex = i + RawReferencePrefix.Length + 1;
                if( value[colonIndex] == ':' ) {
                    string rawMemberPrefix = string.Format( "\"{0}:", value[colonIndex - 1] );
                    value = value.Replace( rawMemberPrefix, "\"" );
                }
            }

            return value;
        }


        private static XDocument GetXmlDocFile( Assembly assembly ) {
            Ensure.NotNull( assembly );

            string fileName = System.IO.Path.GetFileNameWithoutExtension( assembly.Location ) + ".xml";

            if( !DocumentationCache.ContainsKey( fileName ) ) {
                string assemblyDirPath = System.IO.Path.GetDirectoryName( assembly.Location );
                var docFile =
                    from baseDir in new[] { assemblyDirPath }.Concat( SystemSearchDirectories )
                    from dirName in FallbackDirectoryNames
                    let filePath = CombinePath( baseDir, dirName, fileName )
                    let file = new System.IO.FileInfo( filePath )
                    where file.Exists
                    select file;

                var firstDocFile = docFile.FirstOrDefault( );
                DocumentationCache[fileName] = (firstDocFile == null ? null : XDocument.Load( firstDocFile.FullName ));
            }

            return DocumentationCache[fileName];
        }

        #endregion

    }

}
